【SwiftUI】NavigationStackとTabViewとPickerを組み合わせて使用すると何故かタブバーが透明になる問題
NavigationStack
の入れ子にTabView
を配置し、そのTabView
内の子ViewにPicker
を配置すると、何故かタブバーが透明になってしまったので対処法を模索しました。
環境
- Xcode 14.2
- iOS 16.1
現象
現象①
タブバーの背景が透明になってしまいます。
コード
import SwiftUI struct ContentView: View { var body: some View { NavigationStack { TabView { ListView() .tabItem { Image(systemName: "1.circle") } Text("") .tabItem { Image(systemName: "2.circle") } Text("") .tabItem { Image(systemName: "3.circle") } } .navigationTitle("List") } } } // 子View struct ListView: View { enum Category: String, Identifiable, CaseIterable { case wait case inProgress case done var id: String { return self.rawValue } } @State private var category: Category = .wait var body: some View { VStack { Picker("", selection: $category) { ForEach(Category.allCases) { Text($0.rawValue) .tag($0) } } .pickerStyle(.segmented) .padding() List { ForEach(0..<20) { index in Text("Item") } } } } }
現象②
子ViewのPicker
とList
の位置を入れ替えると、タブバーが表示されるようになります。
コード
// 子View struct ListView: View { enum Category: String, Identifiable, CaseIterable { case wait case inProgress case done var id: String { return self.rawValue } } @State private var category: Category = .wait var body: some View { VStack { // Listを上に変更 List { ForEach(0..<20) { index in Text("Item") } } // Pickerを下に変更 Picker("", selection: $category) { ForEach(Category.allCases) { Text($0.rawValue) .tag($0) } } .pickerStyle(.segmented) .padding() } } }
タブバーが透明になってしまうケースとは
同じケースではないが、Stack Overflowで透明になっているケースを発見しました。
iOS 15から変更になったタブバーの仕様で、中身のコンテンツが短い場合にタブバーは透明になるとのこと。
WWDC21 - What's new in UIKitの中(5:50~)でもiOS 15からのタブバーの変更点について説明されていました。
iOS 15での UI の改良点をいくつか紹介します。
UIToolbar と UITabBar の外観を改良しました。この更新された外観では、下にスクロールすると背景素材が削除され、コンテンツがより視覚的に明確になります。
今回発生しているケースとは違いますが、タブバーが透明になってしまうケースが分かりました。
対応策
コードで強制的に背景をつける
onAppear
時にタブバーに背景を付ける処理を行なっています。
struct ContentView: View { var body: some View { NavigationStack { TabView { ListView() .tabItem { Image(systemName: "1.circle") } Text("") .tabItem { Image(systemName: "2.circle") } Text("") .tabItem { Image(systemName: "3.circle") } } .navigationTitle("List") } .onAppear { // タブバーに背景を付ける let tabBarAppearance = UITabBarAppearance() tabBarAppearance.configureWithDefaultBackground() UITabBar.appearance().scrollEdgeAppearance = tabBarAppearance } } }
結果
背景が反映された状態になりました。
NavigationStackとTabViewの入れ子を入れ替える
WWDC22 - Explore navigation design for iOSの内容を見てみると、そもそもTabView
をNavigationStack
の入れ子にするのがApple的にはアンチパターンのように感じました。
タブバーは最上位のコンテンツを表し、アプリの階層の最上位にある必要があります。
なので、入れ子の箇所を入れ替えてみます。
コード
import SwiftUI struct ContentView: View { var body: some View { TabView { // NavigationStackを入れ子にする NavigationStack { ListView() } .tabItem { Image(systemName: "1.circle") } Text("") .tabItem { Image(systemName: "2.circle") } Text("") .tabItem { Image(systemName: "3.circle") } } } } struct ListView: View { enum Category: String, Identifiable, CaseIterable { case wait case inProgress case done var id: String { return self.rawValue } } @State private var category: Category = .wait var body: some View { VStack { Picker("", selection: $category) { ForEach(Category.allCases) { Text($0.rawValue) .tag($0) } } .pickerStyle(.segmented) .padding() List { ForEach(0..<20) { index in Text("Item") } } } .navigationTitle("List") } }
結果
こちらでも同様にタブバーの背景が表示されました。
UITabBarAppearance
を操作する方法より、こちらの方がより自然なのでこちらを採用したいと思います。
おわりに
原因の特定は出来ませんでしたが、挙動的にSwiftUI側のバグのような気はします。
今回対応方法を模索していく中でApple側が意図しているTabViewの使い方を理解することが出来て良かったです。
NavigationStack
(またはNavigationView
)とTabView
を合わせて使用した時におかしな挙動をする時は、入れ子の順序を変更してみると良いかもしれないです。